﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace NMemcached.Client.Binary
{
    /// <summary>
    /// Writes primitive types in binary with the specified endianness to a stream and supports writing strings in a specific encoding.
    /// </summary>
    internal class EndianBinaryWriter : BinaryWriter
    {
        /// <summary>
        /// Gets or sets the endian mode of the binary writer
        /// </summary>
        public Endian Endian { get; set; }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryWriter class
        /// </summary>
        protected EndianBinaryWriter()
            : base()
        {
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryWriter class based on the supplied stream.
        /// </summary>
        /// <param name="output">The stream to which data is written</param>
        public EndianBinaryWriter(Stream output)
            : this(output, Endian.Big)
        {
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryWriter class based on the supplied stream and specific character encoding.
        /// </summary>
        /// <param name="output">The stream to which data is written</param>
        /// <param name="encoding">The character encoding for strings written to the output</param>
        public EndianBinaryWriter(Stream output, Encoding encoding)
            : this(output, encoding, Endian.Big)
        {
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryWriter class based on the supplied stream and endianness.
        /// </summary>
        /// <param name="output">The stream to which data is written</param>
        /// <param name="endian">A value the determines the mode of operation for the binary writer</param>
        public EndianBinaryWriter(Stream output, Endian endian)
            : base(output)
        {
            this.Endian = endian;
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryWriter class based on the supplied stream, a specific character encoding, and endianness.
        /// </summary>
        /// <param name="output">The stream to which data is written</param>
        /// <param name="encoding">The character encoding for strings written to the output</param>
        /// <param name="endian">A value the determines the mode of operation for the binary writer</param>
        public EndianBinaryWriter(Stream output, Encoding encoding, Endian endian)
            : base(output, encoding)
        {
            this.Endian = endian;
        }

        /// <summary>
        /// Writes a decimal value and advances the stream position by 16 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(decimal value)
        {
            int[] bits = Decimal.GetBits(value);
            List<byte> bytes = new List<byte>();
            foreach (int bit in bits)
            {
                bytes.AddRange(BitConverter.GetBytes(bit));
            }
            this.WriteInternal(bytes.ToArray());
        }

        /// <summary>
        /// Writes a double precision value and advances the stream position by 8 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(double value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes a signed short integer value and advances the stream position by 2 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(short value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes a signed integer value and advances the stream position by 4 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(int value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes a signed long integer value and advances the stream position by 8 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(long value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes a single precision value and advances the stream position by 4 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(float value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes an unsigned short integer value and advances the stream position by 2 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(ushort value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes an unsinged integer value and advances the stream position by 4 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(uint value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        /// <summary>
        /// Writes an unsigned long integer value and advances the stream position by 8 bytes
        /// </summary>
        /// <param name="value">The value to write</param>
        public override void Write(ulong value)
        {
            this.WriteInternal(BitConverter.GetBytes(value));
        }

        private void WriteInternal(byte[] bytes)
        {
            if (bytes == null)
            {
                throw new ArgumentNullException("bytes");
            }

            if ((BitConverter.IsLittleEndian && this.Endian == Endian.Big)
                || (!BitConverter.IsLittleEndian && this.Endian != Endian.Big))
            {
                Array.Reverse(bytes);
            }
            base.Write(bytes);
        }
    }
}